import { expect } from 'chai';
import * as step from '@flow-step/step-toolkit'
import sinon from 'sinon';
import { deleteOriginBranch,checkoutCode,mergeCode,pushCode,updateAoneFlowContext } from '../src/merge';


const workDir = 'workspace';

describe('checkoutCode', function() {
    let envStub: sinon.SinonStub;
    let git: any;

    beforeEach(() => {
        envStub = sinon.stub(process, 'env')
        git = {
            clean: sinon.stub().resolves(),
            reset: sinon.stub().resolves(),
            branchLocal: sinon.stub().resolves({ all: ['master'] }),
            checkout: sinon.stub().resolves(),
            pull: sinon.stub().resolves(),
            branch: sinon.stub().resolves(),
        };
    });

    afterEach(() => {
        sinon.restore();
    });

    it('should clean and reset if CLEAN_CACHE is true', async function() {
        envStub.value({'CLEAN_CACHE':'true'})

        await checkoutCode(git);

        expect(git.clean.calledWith(['-dfX'])).to.be.true;
        expect(git.reset.calledWith(['--hard', 'HEAD'])).to.be.true;
    });

    it('should checkout to an existing branch', async function() {
        envStub.value({'TARGET_BRANCH':'target-branch'})
        git.branchLocal.resolves({ all: ['target-branch','master'] });

        await checkoutCode(git);

        expect(git.checkout.calledOnceWith('target-branch')).to.be.true;
        expect(git.pull.calledOnce).to.be.true;
    });

    it('should create a new branch and checkout if target branch does not exist on local', async function() {
        envStub.value({'TARGET_BRANCH':'target-branch'})
        git.branchLocal.resolves({ all: ['feature-1','master'] });

        await checkoutCode(git);

        expect(git.checkout.calledOnceWith(['origin/target-branch', '-b', 'target-branch'])).to.be.true;
        expect(git.pull.calledOnce).to.be.true;
    });

    it('should throw an error if the target branch does not exist on origin and local', async function() {
        envStub.value({'TARGET_BRANCH':'target-branch'})
        git.branchLocal.resolves({ all: ['feature-1','master'] });
        git.branch.resolves({ all: ['feature-1','master'] });
        git.checkout.rejects(new Error('did not match any origin branch'));

        try {
            await checkoutCode(git);
        } catch (error) {
            expect(git.pull.notCalled).to.be.true;
            expect(git.branch.calledOnce).to.be.true;
        }
    });
});



describe('mergeCode', () => {
    let git: any;
    let stepInfoStub: sinon.SinonStub;
    let stepErrorStub: sinon.SinonStub;
    beforeEach(() => {
        stepInfoStub = sinon.stub(step, 'info');
        stepErrorStub = sinon.stub(step, 'error');
        git = {
            merge: sinon.stub().resolves(),
            branch: sinon.stub().resolves(),
        };
    });

    afterEach(() => {
        sinon.restore();
    });

    it('should merge the given branch successfully', async () => {
        git.merge.resolves("OK")
        await mergeCode(git, 'feature-branch');

        expect(stepInfoStub.calledOnceWith('git merge feature-branch')).to.be.true;
        expect(git.merge.calledOnceWith(['feature-branch'])).to.be.true;
    });

    it('should handle merge conflicts', async () => {
        git.merge.rejects(new Error('CONFLICTS: README.md.content'))
        try {
            await mergeCode(git, 'feature-branch');
        }catch (err){
            expect(stepErrorStub.calledWith('Merge conflict, please try merging origin/undefined to feature-branch and pushing again.')).to.be.true;
        }
    });

    it('should handle "not something we can merge" error', async () => {
        git.merge.rejects({
            message: "not something we can merge"
        });
        try {
            await mergeCode(git, 'non-existent-branch');
        } catch (error) {
            expect(stepErrorStub.calledWith('Branch non-existent-branch does not exist, available branches:')).to.be.true;
            expect(git.branch.calledOnce).to.be.true
        }
    });

    it('should rethrow other errors', async () => {
        const error = new Error('Some other error');
        git.merge.rejects(error);
        try {
            await mergeCode(git, 'feature-branch');
        } catch (err) {
            expect(err).to.equal(error);
        }
    });
});


describe('pushCode', function () {
    let envStub: sinon.SinonStub;
    let git: any;
    let stepInfoStub: sinon.SinonStub;
    let stepErrorStub: sinon.SinonStub;
    beforeEach(() => {
        envStub = sinon.stub(process, 'env')
            .value({'TARGET_BRANCH':'test-branch'})
        stepInfoStub = sinon.stub(step, 'info');
        stepErrorStub = sinon.stub(step, 'error');
        git = {
            push: sinon.stub().resolves()
        }
    });

    afterEach(() => {
        sinon.restore();
    });

    it('should push code to the repository correctly (First way)', async function () {
        git.push.resolves();
        await pushCode(git);

        expect(stepInfoStub.calledWith('git push origin test-branch')).to.be.true;
        expect(git.push.calledWith('origin', 'test-branch', ['-o', 'review=no'])).to.be.true;
    });

    it('should attempt a second push if the first fails and succeed', async function () {
        git.push.onFirstCall().rejects();
        git.push.onSecondCall().resolves();

        await pushCode(git);

        expect(git.push.calledWith('origin', 'test-branch', ['-o', 'review=no'])).to.be.true;
        expect(git.push.calledWith('origin', 'test-branch', ['-o'])).to.be.true;
    });

    it('should throw an error if both push attempts fail with a specific message', async function () {
        envStub.value({'GIT_USERNAME':'test-user'})
        const error = new Error('repository not found');
        git.push.rejects(error);

        let caughtError = null;
        try {
            await pushCode(git);
        } catch (e:any) {
            expect(e.message).to.equal(error.message);
            expect(stepErrorStub.calledWith(`repository does not exist, or the user ${process.env.GIT_USERNAME} has no permission`)).to.be.true;
        }
    });
});


describe('deleteOriginBranch', () => {
    let envStub: sinon.SinonStub;
    let deleteBranchWithFilterStub: sinon.SinonStub;
    let git: any;

    beforeEach(() => {
        deleteBranchWithFilterStub = sinon.stub(require('../src/deleteBranchWithFilter'), 'deleteBranchWithFilter');
        envStub = sinon.stub(process, 'env');
        git = {
            push: sinon.stub().resolves()
        }
    });

    afterEach(() => {
       sinon.restore();
    });

    it('should not delete branch if DELETE_ORIGIN_BRANCH is not true', async () => {
        envStub.value({'DELETE_ORIGIN_BRANCH':'false'})
        await deleteOriginBranch(git, 'feature/testBranch');
        expect(deleteBranchWithFilterStub.called).to.be.false;
    });

    it('should delete branch if DELETE_ORIGIN_BRANCH is true', async () => {
        envStub.value({'DELETE_ORIGIN_BRANCH':'true'})
        await deleteOriginBranch(git, 'feature/testBranch');
        expect(deleteBranchWithFilterStub.calledWith(git, 'feature/testBranch')).to.be.true;
    });
});



describe('updateAoneFlowContext', () => {
    let envStub: sinon.SinonStub;
    let git: any;
    let deleteBranchStub: sinon.SinonStub;
    let stepInfoStub: sinon.SinonStub;
    let stepErrorStub: sinon.SinonStub;
    let addOutputStub: sinon.SinonStub;
    const branchRepoInfo = JSON.stringify([{
        name: "testRepo",
        sign: "testRepo",
        repo: "https://git.repo.url",
        baseBranch: { branchName: "master" },
        featureBranchs: [{ branchName: "feature1", commitId: "abcd123" }]
    }]);
    let fsStub: any;

    beforeEach(() => {
        fsStub = {
            appendFile: sinon.stub()
        };
        deleteBranchStub = sinon.stub(require('../src/deleteBranchWithFilter'), 'deleteBranchWithFilter');
        addOutputStub = sinon.stub(step, 'addOutput');
        envStub = sinon.stub(process, 'env').value({
            'branchRepoInfo': branchRepoInfo,
            'WORK_SPACE':  `${workDir}`,
            'PROJECT_DIR': `${workDir}/testRepo`,
            'TARGET_BRANCH': 'master',
            'DELETE_ORIGIN_BRANCH': 'true',
        });
        stepInfoStub = sinon.stub(step, 'info');
        stepErrorStub = sinon.stub(step, 'error');
        git = {
            push: sinon.stub().resolves()
        }
    });

    afterEach(() => {
        sinon.restore();
    });

    it('should parse branch info and append commit info to params file', async () => {
        fsStub.appendFile.resolves(null);

        await updateAoneFlowContext(git);

        const expectedCommitInfo = 'https://git.repo.url.COMMIT_INFO=feature1:abcd123:MERGED';
        expect(addOutputStub.calledWith(expectedCommitInfo)).to.be.true;
        expect(deleteBranchStub.calledOnceWith(git, 'feature1')).to.be.true;
    });

    it('should delete feature branches if DELETE_ORIGIN_BRANCH is true', async () => {
        fsStub.appendFile.resolves(null);

        await updateAoneFlowContext(git);
        expect(deleteBranchStub.calledOnceWith(git, 'feature1')).to.be.true;
    });

    it('should log error if appending to the file fails', async () => {
        const error = new Error("append failed");
        fsStub.appendFile.rejects(error);


        try {
            await updateAoneFlowContext(git);
        } catch (e) {
            expect(e).to.equal(error);
            expect(stepErrorStub.calledOnceWith('Error appending to the file params')).to.be.true;
        }
    });

    it('should not proceed if branchRepoInfo is undefined', async () => {
        envStub.value({
            'WORK_SPACE':  `${workDir}`,
            'PROJECT_DIR': `${workDir}/testRepo`,
            'TARGET_BRANCH': 'master',
            'DELETE_ORIGIN_BRANCH': 'true',
        });

        await updateAoneFlowContext(git);

        expect(stepInfoStub.notCalled).to.be.true;
        expect(fsStub.appendFile.notCalled).to.be.true;
        expect(deleteBranchStub.notCalled).to.be.true;
    });

    it('should use sign if name in branchRepoInfo is empty', async () => {
        fsStub.appendFile.resolves(null);

        let branchRepoInfo2 = JSON.stringify([{
            sign: "testRepo",
            repo: "https://git.repo.url",
            baseBranch: { branchName: "master" },
            featureBranchs: [{ branchName: "feature1", commitId: "abcd123" }]
        }]);
        envStub.value({
            'branchRepoInfo': branchRepoInfo2,
            'WORK_SPACE':  `${workDir}`,
            'PROJECT_DIR': `${workDir}/testRepo`,
            'TARGET_BRANCH': 'master',
            'DELETE_ORIGIN_BRANCH': 'true',
        })

        await updateAoneFlowContext(git);
        expect(deleteBranchStub.calledOnceWith(git, 'feature1')).to.be.true;
    });
});
